// Package gua 不要修改这个包中的任何内容。它是主要作用是对guacd指令的封装和解析，受guacd版本的影响
package gua

import (
	"bufio"
	"errors"
	"fmt"
	"net"
	"strings"
	"time"
)

const (
	TunnelClosed             int = -1
	Normal                   int = 0
	NotFoundSession          int = 800
	NewTunnelError           int = 801
	ForcedBreak              int = 802
	AccessGatewayUnAvailable int = 803
	AccessGatewayCreateError int = 804
	AssetNotActive           int = 805
	ForcedDestroy            int = 806
)

const (
	Delimiter = ';'             // guacamole 协议分隔符
	Version   = "VERSION_1_4_0" // 版本信息

	Hostname = "hostname"
	Port     = "port"
	Username = "username"
	Password = "password"

	Domain         = "domain"          // 域，如果存在的话
	RemoteApp      = "remote-app"      // remote app
	RemoteAppDir   = "remote-app-dir"  // remote app 程序运行的工作目录，需要先开启 remote-app 才能生效
	RemoteAppArgs  = "remote-app-args" // remote app 程序运行的命令行参数，需要先开启 remote-app 才能生效
	StaticChannels = "static-channels" // 自定义虚拟通道,多个通道使用,分割，通道名称限制为7个字符内
	Security       = "security"        // 默认 any
	IgnoreCert     = "ignore-cert"     // true 时忽略证书
	// 画面相关
	ResizeMethod  = "resize-method"  // reconnect
	ForceLossless = "force-lossless" // 是否仅使用无损压缩进行图形更新，建议false，

	// 重定向相关
	DisableAudio     = "disable-audio"      // 禁用音频重定向,默认 false
	EnableAudioInput = "enable-audio-input" // 启用麦克风，默认 false
	EnableTouch      = "enable-touch"       // 启用多点触控，默认false
	enablePrinting   = "enable-printing"    // 启用打印机重定向，默认false。该功能是通过 GhostScript将要打印的文件转换为pdf发送给浏览器，不是真的打印,我们用不到
	printerName      = "printer-name"       // 打印机名称，需要enable-printing=true时生效,我们用不到
	enableDrive      = "enable-drive"       // 启用磁盘重定向，默认false，它不是真的磁盘重定向到应用服务器,我们用不到
	driveName        = "drive-name"         // 磁盘重定向的盘符名称，需要enable-drive=true时生效，我们用不到

	// 录音录像相关
	//EnableRecording     = "enable-recording" // 该项可能被移除了，待定
	RecordingPath       = "recording-path"        // 屏幕录像文件的存储路径，若为空则不开启该功能
	CreateRecordingPath = "create-recording-path" // 是否需要自动创建目录，设置为true时如果目录不存在就自动创建，需要recording-path设置时生效

	// ssh、telnet 等终端仿真器专用
	FontName     = "font-name"     // 字体
	FontSize     = "font-size"     // 大小
	ColorScheme  = "color-scheme"  // 颜色
	Backspace    = "backspace"     // 背景色
	TerminalType = "terminal-type" // 终端仿真器类型，默认值 linux

	// rdp 专用配置，默认都是关闭的。开启后会提升美观效果，但是会降低性能，增加带宽消耗。
	EnableWallpaper          = "enable-wallpaper"           // 启用桌面壁纸渲染，默认false
	EnableFontSmoothing      = "enable-font-smoothing"      // 如果设置为“true”，文本将以平滑的边缘呈现。默认情况下，RDP 上的文本以粗糙的边缘呈现，因为这会减少文本使用的颜色数量，从而减少连接所需的带宽。
	EnableTheming            = "enable-theming"             // 启用窗口和控件的主题，默认为false
	EnableFullWindowDrag     = "enable-full-window-drag"    // 如果设置为“true”，则窗口的内容将在移动窗口时显示。默认情况下，RDP 服务器只会在拖动窗口时绘制窗口边框。
	EnableDesktopComposition = "enable-desktop-composition" // 如果设置为“true”，则允许使用透明窗口和阴影等图形效果。
	EnableMenuAnimations     = "enable-menu-animations"     // 如果设置为“true”，将允许菜单打开和关闭动画
	DisableBitmapCaching     = "disable-bitmap-caching"     // 禁用rdp内置的位图缓存功能，设置为true时不会使用rdp位图缓存功能
	disableOffscreenCaching  = "disable-offscreen-caching"  // 如果设置为“true”，rdp会禁用客户端不可见的屏幕区域的缓存，大多数情况下这个配置没啥用
	disableGlyphCaching      = "disable-glyph-caching"      // 如果设置为“true”，rdp会禁用常用符号和字体缓存。没啥用，FreeRDP 2.0.0 版本起不在支持

	// vnc 专用配置，暂时用不到
	Cursor      = "cursor"
	SwapRedBlue = "swap-red-blue"
	DestHost    = "dest-host"
	DestPort    = "dest-port"

	// telnet 专用配置，暂时用不到
	UsernameRegex     = "username-regex"
	PasswordRegex     = "password-regex"
	LoginSuccessRegex = "login-success-regex"
	LoginFailureRegex = "login-failure-regex"

	// Kubernetes 专用配置，暂时用不到
	Namespace  = "namespace"
	Pod        = "pod"
	Container  = "container"
	UesSSL     = "use-ssl"
	ClientCert = "client-cert"
	ClientKey  = "client-key"
	CaCert     = "ca-cert"
)

type Configuration struct {
	ConnectionID string
	Protocol     string
	Parameters   map[string]string
}

func NewConfiguration() (config *Configuration) {
	config = &Configuration{}
	config.Parameters = make(map[string]string)
	return config
}

func (opt *Configuration) SetParameter(name, value string) {
	opt.Parameters[name] = value
}

func (opt *Configuration) UnSetParameter(name string) {
	delete(opt.Parameters, name)
}

func (opt *Configuration) GetParameter(name string) string {
	return opt.Parameters[name]
}

type Instruction struct {
	Opcode       string
	Args         []string
	ProtocolForm string
}

func NewInstruction(opcode string, args ...string) (ret Instruction) {
	ret.Opcode = opcode
	ret.Args = args
	return ret
}

func (opt *Instruction) String() string {
	if len(opt.ProtocolForm) > 0 {
		return opt.ProtocolForm
	}

	opt.ProtocolForm = fmt.Sprintf("%d.%s", len(opt.Opcode), opt.Opcode)
	for _, value := range opt.Args {
		opt.ProtocolForm += fmt.Sprintf(",%d.%s", len(value), value)
	}
	opt.ProtocolForm += string(Delimiter)
	return opt.ProtocolForm
}

func (opt *Instruction) Parse(content string) Instruction {
	if strings.LastIndex(content, ";") > 0 {
		content = strings.TrimRight(content, ";")
	}
	messages := strings.Split(content, ",")

	var args = make([]string, len(messages))
	for i := range messages {
		lm := strings.Split(messages[i], ".")
		args[i] = lm[1]
	}
	return NewInstruction(args[0], args[1:]...)
}

type Tunnel struct {
	conn   net.Conn
	reader *bufio.Reader
	writer *bufio.Writer
	UUID   string
	Config *Configuration
	IsOpen bool
}

func NewTunnel(address string, config *Configuration) (ret *Tunnel, err error) {
	conn, err := net.DialTimeout("tcp", address, 10*time.Second)
	if err != nil {
		return
	}

	ret = &Tunnel{}
	ret.conn = conn
	ret.reader = bufio.NewReader(conn)
	ret.writer = bufio.NewWriter(conn)
	ret.Config = config

	selectArg := config.ConnectionID
	if selectArg == "" {
		selectArg = config.Protocol
	}

	if err = ret.WriteInstructionAndFlush(NewInstruction("select", selectArg)); err != nil {
		_ = conn.Close()
		return nil, err
	}
	args, err := ret.expect("args")
	if err != nil {
		_ = conn.Close()
		return
	}
	width := config.GetParameter("width")
	height := config.GetParameter("height")
	dpi := config.GetParameter("dpi")
	// send size
	if err := ret.WriteInstructionAndFlush(NewInstruction("size", width, height, dpi)); err != nil {
		_ = conn.Close()
		return nil, err
	}
	if err := ret.WriteInstructionAndFlush(NewInstruction("audio", "audio/L8", "audio/L16")); err != nil {
		_ = conn.Close()
		return nil, err
	}
	if err := ret.WriteInstructionAndFlush(NewInstruction("video")); err != nil {
		_ = conn.Close()
		return nil, err
	}
	if err := ret.WriteInstructionAndFlush(NewInstruction("image", "image/jpeg", "image/png", "image/webp")); err != nil {
		_ = conn.Close()
		return nil, err
	}
	if err := ret.WriteInstructionAndFlush(NewInstruction("timezone", "Asia/Shanghai")); err != nil {
		_ = conn.Close()
		return nil, err
	}
	parameters := make([]string, len(args.Args))
	for i := range args.Args {
		argName := args.Args[i]
		if strings.Contains(argName, "VERSION") {
			parameters[i] = Version
			continue
		}
		parameters[i] = config.GetParameter(argName)
	}
	// send connect
	if err := ret.WriteInstructionAndFlush(NewInstruction("connect", parameters...)); err != nil {
		_ = conn.Close()
		return nil, err
	}
	ready, err := ret.expect("ready")
	if err != nil {
		return
	}
	if len(ready.Args) == 0 {
		_ = conn.Close()
		return nil, errors.New("no connection id received")
	}
	ret.UUID = ready.Args[0]
	ret.IsOpen = true
	return ret, nil
}

func (opt *Tunnel) WriteInstructionAndFlush(instruction Instruction) error {
	if _, err := opt.WriteAndFlush([]byte(instruction.String())); err != nil {
		return err
	}
	return nil
}

func (opt *Tunnel) WriteAndFlush(p []byte) (int, error) {
	//fmt.Printf("-> %v\n", string(p))
	nn, err := opt.writer.Write(p)
	if err != nil {
		return nn, err
	}
	err = opt.writer.Flush()
	if err != nil {
		return nn, err
	}
	return nn, nil
}

func (opt *Tunnel) ReadInstruction() (instruction Instruction, err error) {
	msg, err := opt.Read()
	if err != nil {
		return instruction, err
	}
	return instruction.Parse(string(msg)), err
}

func (opt *Tunnel) Read() (p []byte, err error) {
	data, err := opt.reader.ReadBytes(Delimiter)
	if err != nil {
		return
	}
	s := string(data)
	//fmt.Printf("<- %v \n", s)
	if s == "rate=44100,channels=2;" {
		return make([]byte, 0), nil
	}
	if s == "rate=22050,channels=2;" {
		return make([]byte, 0), nil
	}
	if s == "5.audio,1.1,31.audio/L16;" {
		s += "rate=44100,channels=2;"
	}
	return []byte(s), err
}

func (opt *Tunnel) expect(opcode string) (instruction Instruction, err error) {
	instruction, err = opt.ReadInstruction()
	if err != nil {
		return instruction, err
	}

	if opcode != instruction.Opcode {
		msg := fmt.Sprintf(`expected "%s" instruction but instead received "%s"`, opcode, instruction.Opcode)
		return instruction, errors.New(msg)
	}
	return instruction, nil
}

func (opt *Tunnel) Close() error {
	opt.IsOpen = false
	return opt.conn.Close()
}
